iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
JavaScript

React 觀念架構:從js 基礎到Hook 底層邏輯 系列 第 16 篇

Day 16 useState 底層原理與 State Immutability(不可變性)

  • 分享至 

  • xImage
  •  

今天我們要重新檢視 React 開發中最基礎、卻也最容易踩坑的 Hook:useState。我們將從底層的單向鏈表結構,聊到為什麼 React 堅持 Immutability(不可變性)。

一、 useState 的底層儲存結構:單向鏈表 (Linked List)

在前幾天,我們用閉包簡單模擬了 useState 的運作。但在真實的 React Fiber 架構中,useState 的 State 到底是存放在哪裡的?

答案是:存放在組件對應的 Fiber 節點上的 memoizedState 鏈表中。

[Fiber Node]
   └── memoizedState ──► [Hook 1: useState]
                             └── next ──► [Hook 2: useEffect]
                                              └── next ──► [Hook 3: useState]
                                              

為什麼 Hook 不能寫在 if 或 for 迴圈內?
每次呼叫 useState 時,React 會順著鏈表上的 next 指標依序取出對應的 Hook 節點。如果將 Hook 放在條件判斷式中,導致前後兩次渲染的 Hook 呼叫順序不一致,指標就會錯位,取出錯誤的 State 數據!

二、 核心教條:State 為什麼必須保持不可變 (Immutable)?

在 React 中更新狀態時,我們絕不能直接修改原本的物件或陣列,而必須傳入一個全新的物件/陣列參考 (Reference)。

效能優化:Object.is 同值比對
當你呼叫 setState 時,React 會呼叫內部的 Object.is(oldState, newState) 來比較前後狀態。

  • 若直接修改 (Mutation):oldState 與 newState 指向同一塊記憶體位址,Object.is 回傳 true -> React 認定狀態未改變,直接跳過 (Bailout) Re-render!

  • 傳入新物件 (Immutable):newState 擁有全新的記憶體位址,Object.is 回傳 false -> 成功觸發 Re-render。

const [user, setUser] = useState({ name: 'Alice', score: 100 });

//  錯誤:直接修改原物件,記憶體參考未變,畫面不會更新!
user.score = 200;
setUser(user);

//  正確:透過展開運算子建立全新的物件參考
setUser({
  ...user,
  score: 200
});

三、 兩大坑點拆解

  1. 批量更新與函數式更新 (Functional Update)
    當你需要基於「前一次的 State」連續進行多次更新時,請務必使用 Updater Function:
const [count, setCount] = useState(0);

const handleClick = () => {
  //  三次呼叫都拿到當前閉包快照中的 count (0),最終只加 1
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);

  // 傳入函數,React 會將更新放入 Queue 依序執行,最終加 3
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
};
  1. 惰性初始值 (Lazy Initial State)
    如果 useState 的初始值需要經過昂貴的計算(例如讀取 localStorage 或複雜轉譯),切勿直接將函數呼叫寫在 useState() 參數內:
//  每次組件 Re-render 都會重複執行昂貴的 parse 計算
const [data, setData] = useState(parseExpensiveData());

//  傳入初始化函數 (Lazy Initial State),該計算只會在 Mount 時執行一次!
const [data, setData] = useState(() => parseExpensiveData());

上一篇
Day 15 Concurrent React 實戰應用:useTransition 與 useDeferredValue 防手震切換(Debounce)
下一篇
Day 17 useEffect 生命週期與 Sync 同步觀念
系列文
React 觀念架構:從js 基礎到Hook 底層邏輯 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言